package com.jhf.youke.wechat.app.executor;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.PemUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.ijpay.core.IJPayHttpResponse;
import com.ijpay.core.enums.RequestMethod;
import com.ijpay.core.kit.HttpKit;
import com.ijpay.core.kit.PayKit;
import com.ijpay.core.kit.WxPayKit;
import com.ijpay.core.utils.DateTimeZoneUtil;
import com.ijpay.wxpay.WxPayApi;
import com.ijpay.wxpay.enums.WxDomainEnum;
import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
import com.ijpay.wxpay.model.v3.*;
import com.jhf.youke.core.ddd.BaseEvent;
import com.jhf.youke.core.entity.Message;
import com.jhf.youke.core.utils.CacheUtils;
import com.jhf.youke.core.utils.Constant;
import com.jhf.youke.core.utils.StringUtils;
import com.jhf.youke.wechat.app.constant.WxConstant;
import com.jhf.youke.wechat.domain.event.WxPayEvent;
import com.jhf.youke.wechat.domain.model.dto.WxPayDto;
import com.jhf.youke.wechat.domain.model.dto.WxRefundDto;
import com.jhf.youke.wechat.domain.model.po.AccountPo;
import com.jhf.youke.wechat.domain.model.vo.AccessTokenVo;
import com.jhf.youke.wechat.domain.model.vo.AccountVo;
import com.jhf.youke.wechat.domain.model.vo.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.*;

/**
 * description:
 * date: 2022/10/12 9:23
 * @author: cyx
 */
@Slf4j
@Service
public class WxAppService {

    @Resource
    private AccountAppService accountAppService;

    @Resource
    private ApplicationContext applicationContext;

    public String getOpenIdByCode(String code) {
        AccountVo accountVo = accountAppService.getAppletConfig();
        String url = String.format(WxConstant.GET_OAUTH_OPEN_ID, accountVo.getAppId(), accountVo.getAppSecret(), code);
        Map<String, String> map = new HashMap<>(8);
        map.put(Header.ACCEPT.getValue(), "application/json");
        map.put(Header.CONTENT_TYPE.getValue(), "application/json");
        String json = HttpRequest.get(url)
                .addHeaders(map)
                .timeout(20000)
                .execute().body();
        JSONObject dataMap = JSONUtil.parseObj(json);
        return Convert.toStr(dataMap.get("openid"));
    }

    public Map<String, String> jsApiPay(WxPayDto dto) {
        AccountVo accountVo = accountAppService.getAppletConfig();
        try {
            String timeExpire = DateTimeZoneUtil.dateToTimeZone(System.currentTimeMillis() + 1000 * 60 * 3);
            int fee = dto.getFee().multiply(BigDecimal.valueOf(100)).intValue();
            UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
                    .setAppid(accountVo.getAppId())
                    .setMchid(accountVo.getMchId())
                    .setOut_trade_no(dto.getOutTradeNo())
                    .setAmount(new Amount().setTotal(fee))
                    .setPayer(new Payer().setOpenid(dto.getOpenId()))
                    .setNotify_url(accountVo.getNotifyUrl())
                    .setTime_expire(timeExpire)
                    .setDescription(dto.getDescription())
                    .setAttach(dto.getAttach());
            PrivateKey merchantPrivateKey = getPrivateKey(accountVo.getPaternerKeyPath());
            log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
            IJPayHttpResponse response = WxPayApi.v3(
                    RequestMethod.POST,
                    WxDomainEnum.CHINA.toString(),
                    BasePayApiEnum.JS_API_PAY.toString(),
                    accountVo.getMchId(),
                    accountVo.getSerialNumber(),
                    null,
                    merchantPrivateKey,
                    JSONUtil.toJsonStr(unifiedOrderModel)
            );
            log.info("统一下单响应 {}", response);
            if (response.getStatus() == WxConstant.OK) {
                InputStream platformCert = getPrivateKeyInputStream(accountVo.getPlatformCertPath());
                // 根据证书序列号查询对应的证书来验证签名结果
                boolean verifySignature = WxPayKit.verifySignature(response, platformCert);
                if (verifySignature) {
                    String body = response.getBody();
                    JSONObject jsonObject = JSONUtil.parseObj(body);
                    String prepayId = jsonObject.getStr("prepay_id");
                    Map<String, String> map = WxPayKit.jsApiCreateSign(accountVo.getAppId(), prepayId, merchantPrivateKey);
                    log.info("唤起支付参数:{}", map);
                    return map;
                }
            } else {
                log.info("支付下单失败:{}", response.getBody());
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("支付下单异常:{}", e.getMessage());
        }
        return null;
    }

    public void payNotify(HttpServletRequest request, HttpServletResponse response) {
        AccountVo accountVo = accountAppService.getAppletConfig();
        Map<String, String> map = new HashMap<>(12);
        try {
            String timestamp = request.getHeader("Wechatpay-Timestamp");
            String nonce = request.getHeader("Wechatpay-Nonce");
            String serialNo = request.getHeader("Wechatpay-Serial");
            String signature = request.getHeader("Wechatpay-Signature");

            log.info("timestamp:{} nonce:{} serialNo:{} signature:{}", timestamp, nonce, serialNo, signature);
            String result = HttpKit.readData(request);
            log.info("支付通知密文 {}", result);

            // 需要通过证书序列号查找对应的证书，verifyNotify 中有验证证书的序列号
            InputStream platformStream = getPrivateKeyInputStream(accountVo.getPlatformCertPath());
            String plainText = WxPayKit.verifyNotify(serialNo, result, signature, nonce, timestamp,
                    accountVo.getApiV3Key(), platformStream);

            log.info("支付通知明文 {}", plainText);

            if (StrUtil.isNotEmpty(plainText)) {
                // 支付成功后逻辑处理
                JSONObject notifyJson = JSONUtil.parseObj(plainText);
                Long orderId = StringUtils.toLong(notifyJson.get("out_trade_no"));
                String attach = StringUtils.chgNull(notifyJson.get("attach"));

                String topic = "createSagaConsume";
                String videoCode = "videoOrder";
                if(videoCode.equals(attach)) {
                    //视频支付不走saga，直接推送至订单服务
                    topic = "immediatelyPaySuccessConsume";
                }
                WxPayEvent event = new WxPayEvent(this, BaseEvent.EVENT_APPLICATION, topic,
                        sagaMsgForPaySuccess(orderId, attach, topic));
                applicationContext.publishEvent(event);
                response.setStatus(200);
                map.put("code", "SUCCESS");
                map.put("message", "SUCCESS");
            } else {
                response.setStatus(500);
                map.put("code", "ERROR");
                map.put("message", "签名错误");
            }
            response.setHeader("Content-type", ContentType.JSON.toString());
            response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8));
            response.flushBuffer();
        } catch (Exception e) {
            log.error("支付回调异常:{}", e.getMessage());
        }
    }

    public Boolean refund(WxRefundDto dto) {
        AccountVo accountVo = accountAppService.getAppletConfig();
        try {
            String outRefundNo = PayKit.generateStr();
            log.info("商户退款单号: {}", outRefundNo);

            int fee = dto.getFee().multiply(BigDecimal.valueOf(100)).intValue();
            List<RefundGoodsDetail> list = new ArrayList<>();
            RefundGoodsDetail refundGoodsDetail = new RefundGoodsDetail()
                    .setMerchant_goods_id(dto.getMerchantGoodsId())
                    .setGoods_name(dto.getGoodsName())
                    .setUnit_price(fee)
                    .setRefund_amount(fee)
                    .setRefund_quantity(1);
            list.add(refundGoodsDetail);

            RefundModel refundModel = new RefundModel()
                    .setOut_refund_no(outRefundNo)
                    .setReason(dto.getReason())
                    .setNotify_url(accountVo.getNotifyUrl())
                    .setAmount(new RefundAmount().setRefund(fee).setTotal(fee).setCurrency("CNY"));
            // 商品详情，可不填
            if (StrUtil.isNotEmpty(dto.getTransactionId())) {
                refundModel.setTransaction_id(dto.getTransactionId());
            }
            if (StrUtil.isNotEmpty(dto.getOutTradeNo())) {
                refundModel.setOut_trade_no(dto.getOutTradeNo());
            }
            log.info("退款参数 {}", JSONUtil.toJsonStr(refundModel));
            PrivateKey merchantPrivateKey = getPrivateKey(accountVo.getPaternerKeyPath());
            IJPayHttpResponse response = WxPayApi.v3(
                    RequestMethod.POST,
                    WxDomainEnum.CHINA.toString(),
                    BasePayApiEnum.REFUND.toString(),
                    accountVo.getMchId(),
                    accountVo.getSerialNumber(),
                    null,
                    merchantPrivateKey,
                    JSONUtil.toJsonStr(refundModel)
            );
            if (response.getStatus() == WxConstant.OK) {
                // 根据证书序列号查询对应的证书来验证签名结果
                InputStream platformStream = getPrivateKeyInputStream(accountVo.getPlatformCertPath());
                boolean verifySignature = WxPayKit.verifySignature(response, platformStream);
                log.info("退款响应 {}", response);
                // 退款响应操作
                return verifySignature;
            } else {
                log.info("微信退款失败:{}", response.getBody());
            }
        } catch (Exception e) {
            log.error("微信支付退款异常:{}", e.getMessage());
        }
        return false;
    }

    private PrivateKey getPrivateKey(String path) throws IOException {
        ClassPathResource classPathResource = new ClassPathResource(path);
        InputStream inputStream = classPathResource.getInputStream();
        return PemUtil.readPemPrivateKey(inputStream);
    }

    private InputStream getPrivateKeyInputStream(String path) throws IOException {
        ClassPathResource classPathResource = new ClassPathResource(path);
        return classPathResource.getInputStream();
    }

    /**
     * @param orderId   订单ID
     * @param attach    附加数据
     * @return
     */
    private String sagaMsgForPaySuccess(Long orderId, String attach, String topic) {
        Message message = new Message();
        message.setTopic(topic);
        message.setGroupName("wechatGroup");
        message.setTag("all");
        Map<String, Object> map = new HashMap<>(16);
        map.put("key" + UUID.randomUUID(), "order_pay_success_" + orderId);
        map.put("code", "immediatelyPaySuccess");
        map.put("bizId", orderId);
        map.put("objectId", 1);
        Map<String, Object> msg = new HashMap<>(8);
        msg.put("orderId", orderId);
        msg.put("attach", attach);
        map.put("message", msg);
        message.setMessages(map);
        return JSONUtil.toJsonStr(message);
    }

    /**
     * 根据openId获取用户微信信息
     * @param param
     * @return
     */
    public UserInfoVo getUserInfoByOpenId(Map<String, Object> param) {
        String openid = StringUtils.chgNull(param.get("openId"));
        AccountVo accountVo = accountAppService.getAppletConfig();
        String url = String.format(WxConstant.GET_OAUTH_USERINFO, getAccessToken(accountVo), openid);
        Map<String, String> map = new HashMap<>(8);
        map.put(Header.ACCEPT.getValue(), "application/json");
        map.put(Header.CONTENT_TYPE.getValue(), "application/json");
        String json = HttpRequest.get(url)
                .addHeaders(map)
                .timeout(20000)
                .execute().body();
        Map<String, Object> dataMap = JSON.parseObject(json, Map.class);
        if (dataMap == null || dataMap.containsKey(Constant.RESPONSE_WX_ERROR_CODE)) {
            return null;
        }
        UserInfoVo vo = new UserInfoVo();
        vo.setOpenid(Convert.toStr(dataMap.get("openid")));
        vo.setNickname(Convert.toStr(dataMap.get("nickname")));
        vo.setHeadimgurl(Convert.toStr(dataMap.get("headimgurl")));
        vo.setSex(Convert.toInt(dataMap.get("sex")));
        return vo;
    }

    /**
     * 获取accessToken
     */
    public String getAccessToken(AccountVo account){
        String key = account.getAppId().concat("_access_token");
        String accessToken = CacheUtils.get(key);
        if (StringUtils.isNotEmpty(accessToken)) {
            return accessToken;
        }
        AccessTokenVo token = getAccessToken(account.getAppId(), account.getAppSecret());
        if(token != null){
            //获取失败
            if(token.getErrcode() != null){
                log.info("## getAccessToken appid = " + account.getAppId());
                log.info("## getAccessToken orgial = " + account.getOriginal());
                log.info("## getAccessToken getAppsecret = " + account.getAppSecret());
                log.info("## getAccessToken Error = " + token.getErrmsg());
                log.info("## getAccessToken Code = " + token.getErrcode());
            }else{
                CacheUtils.set(key, token.getAccessToken(), 7100);
                return token.getAccessToken();
            }
        }
        return null;
    }

    /**
     * 获取接口访问凭证
     */
    public AccessTokenVo getAccessToken(String appId, String appSecret) {
        AccessTokenVo token = null;
        String url = String.format(WxConstant.GET_TOKEN_URL, appId, appSecret);

        Map<String, String> map = new HashMap<>(8);
        map.put(Header.ACCEPT.getValue(), "application/json");
        map.put(Header.CONTENT_TYPE.getValue(), "application/json");
        String json = HttpRequest.get(url)
                .addHeaders(map)
                .timeout(20000)
                .execute().body();
        JSONObject dataMap = JSONUtil.parseObj(json);
        if (null != dataMap && !dataMap.containsKey(Constant.RESPONSE_WX_ERROR_CODE)) {
            try {
                token = new AccessTokenVo();
                token.setAccessToken(dataMap.getStr("access_token"));
                token.setExpiresIn(dataMap.getLong("expires_in"));
            } catch (JSONException e) {
                //获取token失败
                token = null;
            }
        }else if(null != dataMap){
            token = new AccessTokenVo();
            token.setErrcode(dataMap.getInt("errcode"));
        }
        return token;
    }

}
